using System;
using System.Collections.Generic;
using System.Collections;
using System.Text;

namespace ErlyThreads
{
	public class ProcessRuntime
	{
		/// <summary>
		/// The list of threads that this master controls.
		/// </summary>
		private IDictionary<object, ThreadContainerBase> mLockedThreads = null;
		private readonly object mThreadsLock = new object();

		private IDictionary<object, IDictionary<Converter<Type, bool>, object>> mLockedSubscriptions = null;
		private readonly object mSubscriptionsLock = new object();

		// doesn't need to be locked, set up in ctor
		private IProcessHandle mOwnerThreadContainerBase = null;

		private readonly bool mconstSendToAllByDefault;

		private IDictionary<object, IList<ReceivedMessage>> mLockMessagesToNonExistingRecipients = null;
		private readonly object mMessagesToNonExistingRecipientsLock = new object();

		public IProcessHandle ProcessHandle
		{
			get { return mOwnerThreadContainerBase; }
		}

		public ProcessRuntime(
				object pRuntimeId)
			: this(pRuntimeId, false)
		{
		}

		/// <summary>
		/// Constructs a new thread master. It is possible to have multiple 
		/// instances of the ThreadMaster class. Id's are unique to each instance, 
		/// and cross messaging is not supported.
		/// </summary>
		public ProcessRuntime(
				object pRuntimeId,
				bool pSendToAllByDefault)
		{
			mconstSendToAllByDefault = pSendToAllByDefault;
			mLockedThreads = new Dictionary<object, ThreadContainerBase>();
			mLockedSubscriptions = new Dictionary<object, IDictionary<Converter<Type, bool>, object>>();
			mLockMessagesToNonExistingRecipients = new Dictionary<object, IList<ReceivedMessage>>();

			InitProcess(
					pRuntimeId,
					delegate(IProcessHandle pBase)
					{
						throw new ApplicationException(
								"You are not supposed to run the owning thread. It is running in order to own anything");
					});
			mOwnerThreadContainerBase = mLockedThreads[pRuntimeId];
		}

		/// <summary>
		/// Inits a new process. Runs on a normal Thread instance.
		/// </summary>
		/// <param name="pId">The Id for the new process</param>
		/// <param name="pStart">The method that this thread will execute with when it runs.</param>
		public void InitProcess(
				object pId,
				Action<IProcessHandle> pStart)
		{
			InitProcess(
					pId,
					pStart,
					false);
		}

		/// <summary>
		/// Initialises the thread. Does not start it.
		/// </summary>
		/// <param name="pId">Will represent the Id for this thread. 
		/// Used later refer to this thread by "name"</param>
		/// <param name="pStart">Represents the method to run when the thread starts up</param>
		/// <param name="pIsShortTaskHint">Provide a hint for if your process is short-lived or not.
		/// If you say true here, you have a chance that your task will be run on a threadpool thread
		/// instead of with a normal new thread instance.</param>
		public void InitProcess(
				object pId,
				Action<IProcessHandle> pStart,
				bool pHintIsShortTask)
		{
			ThreadContainerBase tbase =
					ThreadContainerBase.ThreadContainerBaseFactory(
							this,
							pStart,
							pId,
							pHintIsShortTask);

			// the purpose of the strange locking pattern here
			// is to force the messages to be sent to the new thread
			// in the same order as they were received
			lock (mThreadsLock)
			{
				mLockedThreads.Add(
						pId,
						tbase);

				lock (mMessagesToNonExistingRecipientsLock)
				{
					if (mLockMessagesToNonExistingRecipients.ContainsKey(pId))
					{
						IList<ReceivedMessage> list = mLockMessagesToNonExistingRecipients[pId];
						mLockMessagesToNonExistingRecipients.Remove(pId);
						foreach (ReceivedMessage msg in list)
						{
							tbase.Message_Receive(msg);
						}
					}
				}
			}

		}

		/// <summary>
		/// Use to send a message to another thread(s)
		/// </summary>
		/// <param name="pId">The "name" of the thread that must receive the message. 
		/// If typeof(pId) is Array, then sends to all threads identified by Ids in the array. 
		/// If null, sends to all. This message will also be sent to all of the subscribers to
		/// this thread's messages.</param>
		/// <param name="pMessage">The "message" that is going to be sent. 
		/// This class must be Serializable, or you will get a runtime exception.</param>
		internal void Message_Send(
				object pIdFrom,
				object pIdTo,
				object pMessage)
		{
			IList<object> deliveredToIds =
					new List<object>();

			if (pIdTo == null)
			{
				if (mconstSendToAllByDefault)
				{
					Message_Send_To_All(
							pIdTo,
							pMessage,
							deliveredToIds);
				}
			}
			else if (pIdTo is IEnumerable<object>)
			{
				Message_Send_To_Group(
						pIdFrom,
						pIdTo as IEnumerable<object>,
						pMessage,
						deliveredToIds);
			}
			else
			{
				Message_Send_To_Single(
						pIdFrom,
						pIdTo,
						pMessage,
						deliveredToIds);
			}

			// we send to the subscribers anyway
			Message_Send_To_Subscribers(
					pIdFrom,
					pMessage,
					deliveredToIds);
		}

		private void Message_Send_To_Single(
				object pIdFrom,
				object pIdTo,
				object pMessage,
				IList<object> pDeliveredIds)
		{
			// don't deliver the same message to the same recipient twice
			// or don't try to deliver to a thread that has not been inited yet
			if (pDeliveredIds.Contains(pIdTo))
			{
				return;
			}

			ReceivedMessage receivedMessage =
					ReceivedMessage.CreateMessage(
							pIdFrom,
							pMessage);

			// we are relying on the principle that a dead-lock
			// will not occur if we acquire locks in the same
			// sequence allways
			// in this case also look at thread start
			// where messages are delivered to newly created
			// threads
			lock (mThreadsLock)
			{
				if (mLockedThreads.ContainsKey(pIdTo))
				{
					mLockedThreads[pIdTo].Message_Receive(
							receivedMessage);
					pDeliveredIds.Add(pIdTo);
				}
				else
				{
					Message_Send_To_Non_Existing_Thread(
							pIdTo,
							receivedMessage,
							pDeliveredIds);
				}
			}
		}

		private void Message_Send_To_Non_Existing_Thread(
				object pIdTo,
				ReceivedMessage pReceivedMessage,
				IList<object> pDeliveredIds)
		{
			lock (mMessagesToNonExistingRecipientsLock)
			{
				IList<ReceivedMessage> list = null;
				if (mLockMessagesToNonExistingRecipients.ContainsKey(pIdTo))
				{
					list = mLockMessagesToNonExistingRecipients[pIdTo];
				}
				else
				{
					mLockMessagesToNonExistingRecipients.Add(
							pIdTo,
							list = new List<ReceivedMessage>());
				}

				list.Add(pReceivedMessage);
			}

			pDeliveredIds.Add(pIdTo);
		}

		private void Message_Send_To_Group(
				object pIdFrom,
				IEnumerable<object> pIdTos,
				object pMessage,
				IList<object> pDeliveredIds)
		{
			foreach (object id in pIdTos)
			{
				Message_Send_To_Single(
						pIdFrom,
						id,
						pMessage,
						pDeliveredIds);
			}
		}

		private void Message_Send_To_All(
				object pIdFrom,
				object pMessage,
				IList<object> pDeliveredIds)
		{
			ICollection<object> threadIds = null;
			lock (mThreadsLock)
			{
				threadIds = mLockedThreads.Keys;
			}

			Message_Send_To_Group(
					pIdFrom,
					threadIds,
					pMessage,
					pDeliveredIds);
		}

		private void Message_Send_To_Subscribers(
				object pIdFrom,
				object pMessage,
				IList<object> pDeliveredIds)
		{
			IDictionary<Converter<Type, bool>, object> subscriptions = null;

			// Make a copy of the subscriptions, and send to those
			lock (mLockedSubscriptions)
			{
				if (mLockedSubscriptions.Keys.Contains(pIdFrom))
				{
					subscriptions = new Dictionary<Converter<Type, bool>, object>();
					foreach (KeyValuePair<Converter<Type, bool>, object> kvp in
							mLockedSubscriptions[pIdFrom])
					{
						subscriptions.Add(new KeyValuePair<Converter<Type, bool>, object>(
								kvp.Key,
								kvp.Value));
					}
				}
			}

			// don't send if nobody subscribed
			if (subscriptions == null)
			{
				return;
			}

			Type messageType = pMessage != null ?
									pMessage.GetType() :
									null;
			foreach (KeyValuePair<Converter<Type, bool>, object> kvp in subscriptions)
			{
				if (kvp.Key(messageType))
				{
					Message_Send_To_Single(
							pIdFrom,
							kvp.Value,
							pMessage,
							pDeliveredIds);
				}
			}
		}

		/// <summary>
		/// Receive all message sent by this thread
		/// </summary>
		/// <param name="pSenderId">The "name" of the thread that will be sending the messages</param>
		/// <param name="pReceiverId">The "name" of the thread that will be receiving the messages</param>
		/// <param name="pFilter">Acts as a filter. You will be passed the messages' type 
		/// that is sent by the thread. Return true, if you want to receive this type of message.</param>
		internal void Messages_Subscribe(
				object pSenderId,
				object pReceiverId,
				Converter<Type, bool> pFilter)
		{
			lock (mSubscriptionsLock)
			{
				IDictionary<Converter<Type, bool>, object> subscriptions = null;

				// make sure that the ThreadMaster knows that the sending thread has subscriptions
				if (!mLockedSubscriptions.Keys.Contains(pSenderId))
				{
					subscriptions = new Dictionary<Converter<Type, bool>, object>();
					mLockedSubscriptions.Add(pSenderId, subscriptions);
				}
				else
				{
					subscriptions = mLockedSubscriptions[pSenderId];
				}

				// add the new subscription
				subscriptions.Add(pFilter, pReceiverId);
			}
		}

		/// <summary>
		/// Starts the thread identified by pId. Throws if that thread is running already.
		/// </summary>
		/// <param name="pId">the Id for the thread that must start.</param>
		public void StartProcess(object pId)
		{
			lock (mThreadsLock)
			{
				mLockedThreads[pId].Start();
			}
		}

	}
}
